package com.wr.hangzhounewlight.opcua;

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.subscriptions.MonitoredItemSynchronizationException;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaMonitoredItem;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaMonitoredItem.DataValueListener;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription.SubscriptionListener;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @author yuanzhizhuo
 * createDate 2025/10/10
 */
@Slf4j
public class OpcUaUtil {

    /**
     * 浏览opc ua路径下的节点
     * <p>
     * 浏览方向：顺引用方向
     * 引用类型：任意类型
     * 是否包含子类型：不包含
     * 返回节点过滤：Variable
     * 返回信息过滤：Unspecified
     * <p>
     * getNodeId()返回的ExpandedNodeId为可跨服务器节点，节点可能不存在于当前服务器，已过滤
     *
     * @param opcUaClient  opc ua客户端
     * @param browseNodeId 浏览起始节点
     * @return 起始节点下的所有节点
     */
    public static List<NodeId> browseNode(OpcUaClient opcUaClient, NodeId browseNodeId) throws ExecutionException, InterruptedException, TimeoutException, UaException {
        OpcUaClient opcUaClientConnected = opcUaClient.connectAsync().get(5, TimeUnit.SECONDS);
        UInteger resultNodeType = UInteger.valueOf(NodeClass.Variable.getValue());
        UInteger resultNodeInfo = UInteger.valueOf(NodeClass.Unspecified.getValue());
        BrowseDescription browseWith = new BrowseDescription(browseNodeId, BrowseDirection.Forward, null, false, resultNodeType, resultNodeInfo);
        BrowseResult browseResult = opcUaClientConnected.browse(browseWith);
        List<NodeId> nodeIdList = new ArrayList<>();
        for (ReferenceDescription reference : browseResult.getReferences()) {
            Optional<NodeId> nodeIdOptional = reference.getNodeId().toNodeId(opcUaClientConnected.getNamespaceTable());
            nodeIdOptional.ifPresent(nodeIdList::add);
        }
        return nodeIdList;
    }

    /**
     * 读取节点的值
     *
     * @param opcUaClient opc ua客户端
     * @param nodeIdList  节点列表
     * @return 取值后的opc ua信息
     */
    public static List<DataValue> readValue(OpcUaClient opcUaClient, List<NodeId> nodeIdList) throws ExecutionException, InterruptedException, TimeoutException {
        OpcUaClient opcUaClientConnected = opcUaClient.connectAsync().get(5, TimeUnit.SECONDS);
        List<DataValue> dataValueList;
        if (nodeIdList.size() > 100) {
            dataValueList = splitNodeIdList(nodeIdList, 100).stream().map(
                    splitNodeId -> readValueExtract(opcUaClientConnected, splitNodeId)
            ).flatMap(List::stream).toList();
        } else {
            dataValueList = readValueExtract(opcUaClientConnected, nodeIdList);
        }
        return dataValueList;
    }

    /**
     * 按照指定大小分割nodeIdList
     *
     * @param nodeIdList 节点列表
     * @param batchSize  每个批次的大小
     * @return 分割后的nodeIdList
     */
    private static List<List<NodeId>> splitNodeIdList(List<NodeId> nodeIdList, int batchSize) {
        return Lists.partition(nodeIdList, batchSize);
    }

    /**
     * 读取节点值
     *
     * @param opcUaClient opc ua客户端
     * @param nodeIdList  节点列表
     * @return 节点值列表
     */
    private static List<DataValue> readValueExtract(OpcUaClient opcUaClient, List<NodeId> nodeIdList) {
        try {
            return opcUaClient.readValues(0.0, TimestampsToReturn.Both, nodeIdList);
        } catch (UaException e) {
            return Collections.nCopies(nodeIdList.size(), new DataValue(Variant.NULL_VALUE, StatusCode.BAD, DateTime.now()));
        }
    }

    /**
     * 写入节点值
     *
     * @param opcUaClient opc ua客户端
     * @param nodeId      节点id
     * @param value       节点值
     * @return 是否写入成功
     */
    public static boolean writeValue(OpcUaClient opcUaClient, NodeId nodeId, Object value) throws UaException {
        return opcUaClient.writeValues(List.of(nodeId), List.of(DataValue.valueOnly(new Variant(value)))).get(0).isGood();
    }

    /**
     * 订阅节点值
     *
     * @param opcUaClient           opc ua客户端
     * @param subscriptionListener  订阅级监听器
     * @param subscribeNodeIdList   订阅节点id列表
     * @param dataValueListenerList 数据值监听器列表
     * @return opc ua订阅
     * @throws UaException opc ua异常
     */
    public static OpcUaSubscription subscribeDataValue(
            OpcUaClient opcUaClient, SubscriptionListener subscriptionListener,
            List<NodeId> subscribeNodeIdList, List<DataValueListener> dataValueListenerList
    ) throws UaException, ExecutionException, InterruptedException, TimeoutException {
        OpcUaClient opcUaClientConnected = opcUaClient.connectAsync().get(5, TimeUnit.SECONDS);
        OpcUaSubscription opcUaSubscription = new OpcUaSubscription(opcUaClientConnected);
        OpcUaUtil.setSubscriptionListener(opcUaSubscription, subscriptionListener);
        for (int i = 0; i < subscribeNodeIdList.size(); i++) {
            OpcUaUtil.setDataValueListener(opcUaSubscription, subscribeNodeIdList.get(i), dataValueListenerList.get(i));
        }
        return opcUaSubscription;
    }

    /**
     * 创建订阅级监听器
     *
     * @param opcUaSubscription    opc ua订阅
     * @param subscriptionListener 订阅级监听器
     * @throws UaException opc ua异常
     */
    private static void setSubscriptionListener(
            OpcUaSubscription opcUaSubscription, SubscriptionListener subscriptionListener
    ) throws UaException {
        opcUaSubscription.setSubscriptionListener(subscriptionListener);
        opcUaSubscription.create();
    }

    /**
     * 创建数据值监听器
     *
     * @param opcUaSubscription opc ua订阅
     * @param listenNodeId      订阅节点id
     * @param dataValueListener 数据值监听器
     * @throws MonitoredItemSynchronizationException 监控项同步异常
     */
    private static void setDataValueListener(
            OpcUaSubscription opcUaSubscription, NodeId listenNodeId, DataValueListener dataValueListener
    ) throws MonitoredItemSynchronizationException {
        OpcUaMonitoredItem opcUaMonitoredItem = OpcUaMonitoredItem.newDataItem(listenNodeId);
        opcUaMonitoredItem.setDataValueListener(dataValueListener);
        opcUaSubscription.addMonitoredItem(opcUaMonitoredItem);
        opcUaSubscription.synchronizeMonitoredItems();
    }

}
